@wonseok-han/confluence-sync 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -167,7 +167,7 @@ export async function runConvert(argv) {
167
167
  }
168
168
  const allMd = collectMarkdown(baseDir); // 링크 해석용(전체)
169
169
  const mdRels = allMd.map((f) => posix(relative(baseDir, f)));
170
- const vault = buildVault(mdRels, collectAssets(baseDir).map((f) => posix(relative(baseDir, f))));
170
+ const vault = buildVault(mdRels, collectAssets(baseDir).map((f) => posix(relative(baseDir, f))), baseDir);
171
171
  // obsidian 방향의 이름 중복 검사도 트리 전체 기준이어야 한다
172
172
  const nameCount = new Map();
173
173
  for (const f of allMd) {
@@ -17,7 +17,7 @@ const FENCE = /^\s*(?:```|~~~)/;
17
17
  * 대상이 **비어 있을 수 있다** — `[[#헤딩]]` 은 "이 문서의 이 섹션"이라는 Obsidian 표기다.
18
18
  * 표 안에서는 별칭 앞 파이프가 `\|` 로 이스케이프되므로 그 백슬래시도 함께 받아 준다.
19
19
  */
20
- export const WIKILINK_RE = /(!?)\[\[([^[\]|#]*)(#[^[\]|]+)?(?:\\?\|([^[\]]*))?\]\]/g;
20
+ export const WIKILINK_RE = /(!?)\[\[((?:(?!\[\[|\]\])[^|#\n])*)(#(?:(?!\[\[|\]\])[^|\n])+)?(?:\\?\|([^[\]]*))?\]\]/g;
21
21
  /**
22
22
  * WIKILINK_RE 의 캡처 그룹을 해석한다. 대상도 섹션도 없으면(`[[]]`) 링크가 아니므로 null.
23
23
  * 표의 `\|` 때문에 앞 그룹 끝에 딸려온 백슬래시는 여기서 떼어낸다.
@@ -55,12 +55,12 @@ export function readDoc(baseDir, rel) {
55
55
  };
56
56
  }
57
57
  /** base 상대 경로 → 제목·앵커 인덱스(링크와 섹션 링크를 해석하는 데 둘 다 필요하다) */
58
- export function buildDocIndex(rels, baseDir) {
58
+ export function buildDocIndex(rels, baseDir, sourceDocs) {
59
59
  const titles = {};
60
60
  const anchors = {};
61
61
  const docs = {};
62
62
  for (const rel of rels) {
63
- const doc = readDoc(baseDir, rel);
63
+ const doc = sourceDocs?.[rel] ?? readDoc(baseDir, rel);
64
64
  titles[rel] = doc.title;
65
65
  anchors[rel] = doc.anchors;
66
66
  docs[rel] = doc;
@@ -98,7 +98,7 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
98
98
  let slug;
99
99
  if (la.wiki) {
100
100
  // dest 가 비면 `[[#헤딩]]` — 같은 문서의 섹션이다
101
- target = la.dest === '' ? rel : (vault.notes[la.dest.toLowerCase().replace(/\.md$/i, '')] ?? null);
101
+ target = la.dest === '' ? rel : vaultPath(vault, la.dest, false, rel);
102
102
  slug = wikilinkAnchorToSlug(la.fragment);
103
103
  }
104
104
  else {
@@ -107,8 +107,10 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
107
107
  target = null;
108
108
  slug = decodeAnchor(la.fragment).toLowerCase();
109
109
  }
110
+ // Reference lookup may be ambiguous even when the vault resolved a nearby note.
111
+ // Keep that local target so preprocessing other notes cannot change anchor hashes.
110
112
  if (localTarget && la.dest)
111
- target = localTarget(rel, la.dest, la.wiki);
113
+ target = localTarget(rel, la.dest, la.wiki) ?? target;
112
114
  if (!target || !slug)
113
115
  continue;
114
116
  // 실제로 존재하는 헤딩만 — 없는 앵커는 markdown.ts 가 경고로 잡는다
@@ -124,30 +126,45 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
124
126
  * Obsidian 은 `[[파일명]]`(짧은 이름)과 `[[폴더/파일명]]`(경로) 을 모두 허용하므로 둘 다 등록한다.
125
127
  * 짧은 이름이 겹치면 경로가 사전순으로 앞서는 쪽이 이긴다(Obsidian 의 "가장 가까운 것" 규칙과는 다르지만 결정적이다).
126
128
  */
127
- export function buildVault(mdRels, assetRels) {
129
+ export function buildVault(mdRels, assetRels, base) {
128
130
  const index = (rels, stripExt) => {
129
131
  const idx = {};
130
132
  for (const rel of [...rels].sort()) {
131
133
  const withoutExt = stripExt ? rel.replace(/\.md$/i, '') : rel;
132
134
  for (const key of [withoutExt, basename(withoutExt)]) {
133
- const k = key.toLowerCase();
135
+ const k = key.normalize('NFC').toLowerCase();
134
136
  if (!(k in idx))
135
137
  idx[k] = rel; // 먼저 등록된(사전순 앞선) 것이 이긴다
136
138
  }
137
139
  }
138
140
  return idx;
139
141
  };
140
- return { notes: index(mdRels, true), assets: index(assetRels, false) };
142
+ return { notes: index(mdRels, true), assets: index(assetRels, false), rootName: base ? basename(base).normalize('NFC').toLowerCase() : undefined };
143
+ }
144
+ /** Resolve a vault-root path without reading outside the selected workspace. */
145
+ export function vaultPath(vault, target, embed = false, from = '') {
146
+ let key = target.normalize('NFC').toLowerCase();
147
+ if (key.startsWith('/') || key.includes('\\') || key.split('/').some(part => part === '..' || part === '.'))
148
+ return null;
149
+ const noteKey = key.replace(/\.md$/i, '');
150
+ const dir = dirname(from).normalize('NFC').toLowerCase();
151
+ const nearby = !key.includes('/') && dir !== '.' ? vault.notes[dir + '/' + noteKey] : undefined;
152
+ const direct = embed ? (vault.assets[key] ?? vault.notes[noteKey]) : (nearby ?? vault.notes[noteKey] ?? vault.assets[key]);
153
+ if (direct)
154
+ return direct;
155
+ if (!vault.rootName)
156
+ return null;
157
+ const parts = key.split('/'), at = parts.lastIndexOf(vault.rootName);
158
+ if (at < 0)
159
+ return null;
160
+ key = parts.slice(at + 1).join('/');
161
+ return embed ? (vault.assets[key] ?? vault.notes[key.replace(/\.md$/i, '')] ?? null) : (vault.notes[key.replace(/\.md$/i, '')] ?? vault.assets[key] ?? null);
141
162
  }
142
163
  /** 파일(rel) 기준으로 wikilink 대상을 상대경로로 풀어주는 resolver. */
143
164
  export function vaultResolver(rel, vault) {
144
165
  const dir = dirname(rel) === '.' ? '' : dirname(rel);
145
166
  return (target, embed) => {
146
- const key = target.toLowerCase().replace(/\.md$/i, '');
147
- // 임베드(![[...]])는 대개 이미지, 링크([[...]])는 대개 노트 → 각자 먼저 찾아본다
148
- const hit = embed
149
- ? (vault.assets[target.toLowerCase()] ?? vault.notes[key])
150
- : (vault.notes[key] ?? vault.assets[target.toLowerCase()]);
167
+ const hit = vaultPath(vault, target, embed, rel);
151
168
  if (!hit)
152
169
  return null;
153
170
  const r = relative(dir || '.', hit).split('\\').join('/');
@@ -46,7 +46,7 @@ const markdownDisplayText = (text) => text.replace(/[\\_]/g, '\\$&');
46
46
  * 코드블록(``` / ~~~)과 인라인 코드(`...`) 안은 그대로 둔다 — 링크처럼 생긴 예제 코드를
47
47
  * 망가뜨리지 않기 위해서다.
48
48
  */
49
- function mapProse(md, fn) {
49
+ export function mapProse(md, fn) {
50
50
  let inFence = false;
51
51
  return md
52
52
  .split('\n')